Fix incorrect selector transformation when a substring of a selector matches another local classname #1505
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes #1501.
The bug
Depending on the hash of a scoped classname, a selector such as
&&
would end up transformed into a selector that was not scoped to the classname represented by&
, which would cause an invalid selector error.An example from the test case I added involves the local classnames
debugName_hash1
anddebugName_hash1d
. The&&
selector would becomedebugName_hash1debugName_hash1
, which containsdebugName_hash1d
as a substring.When this occurs, the local classname search returns 3 results of the form:
Note that we iterate over these search results in reverse order. In the above case, the replacement for the middle element would overlap with characters already replaced by the last element, which causes the bug.
It might not be obvious as to when this specific set of classnames would ever be generated. That is, a classname that both contains a substring equal to another classnae, and both starts and ends with the same character.
As mentioned in #1501, the
refCount
appended to a classname hash increments by 1 for every classname generated in a file. This value is encoded as a base 36 value, so it goes from0-9a-z
, then1(0-9a-z)
, etc.This means that once you roll over to the
a-z
range, you'll eventually end up with a classname that satisfies both of the constraints mentioned above. Ideally you wouldn't need to write a selector like&&
as increasing specificity shouldn't be much of an issue if you're using scoped classnames, but regardless this shouldn't break selector transformation.The Fix
The previous logic of checking
startIndex >= lastReplaceIndex
didn't cover this case. By ensuring that we only ever do a replacement whenlastReplaceIndex > endIndex
, the replacement will only affect characters that have not already been replaced. We actually want to negate this logic as wecontinue
when we don't want to replace. This is how we end up withlastReplaceIndex <= endIndex
as the skip condition.To test out this fix, install
@vanilla-extract/css@fix-buggy-selector-transform
.